Skip to content

Python: make FoundryToolbox.as_skills_provider() disable_caching effective#7135

Open
giles17 wants to merge 4 commits into
microsoft:mainfrom
giles17:fix-toolbox-skills-caching
Open

Python: make FoundryToolbox.as_skills_provider() disable_caching effective#7135
giles17 wants to merge 4 commits into
microsoft:mainfrom
giles17:fix-toolbox-skills-caching

Conversation

@giles17

@giles17 giles17 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

FoundryToolbox.as_skills_provider() accepts a disable_caching parameter, but it had no effect: the toolbox's skill discovery was re-read from skill://index.json over MCP on every agent run, adding an avoidable round-trip per request for hosted agents.

The cause is that as_skills_provider() forwarded disable_caching to SkillsProvider, which deliberately does not auto-wrap a caller-supplied SkillsSource in caching (to avoid leaking skills across agents/tenants for context-aware sources). So the flag was silently ignored, and skills were never cached. The docstring also claimed "caching after the first discovery", which was inaccurate.

Surfaced during review of #7099.

Description & Review Guide

  • What are the major changes?
    • as_skills_provider() now composes caching itself, wrapping the source in DeduplicatingSkillsSource(CachingSkillsSource(...)) when caching is enabled, so disable_caching has a real, observable effect.
    • Stopped forwarding disable_caching to SkillsProvider (it was a no-op there for a caller-supplied source).
    • Added a cache_refresh_interval: timedelta | None parameter, threaded into CachingSkillsSource, so callers can re-query when a toolbox's attached skills change over the process lifetime.
    • Rewrote the docstring to describe the real caching behavior.
    • Added unit tests covering cached (index read once), disabled (read every run), and refresh-interval behavior.
  • What is the impact of these changes?
    • By default, toolbox skill discovery is now cached (skill://index.json read once instead of per run). This is safe because _FoundryToolboxSkillsSource is context-independent: its get_skills ignores the SkillsSourceContext and returns the same toolbox skills for every caller. No public API is removed; disable_caching keeps working and now actually does something.
  • What do you want reviewers to focus on?
    • The safety argument for composing caching here (context-independence of the source).

Related Issue

Fixes #7100

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

…ctive

as_skills_provider() forwarded disable_caching to SkillsProvider, which
ignores it for a caller-supplied SkillsSource, so it was a no-op and the
toolbox re-read skill://index.json on every agent run.

Compose caching in as_skills_provider() instead: wrap the context-independent
_FoundryToolboxSkillsSource in DeduplicatingSkillsSource(CachingSkillsSource(...)).
Add a cache_refresh_interval param, fix the docstring, and add tests covering
cached, disabled, and refresh-interval behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84150ec4-6f7c-4ef8-b9fb-12fa11652773
Copilot AI review requested due to automatic review settings July 15, 2026 21:35
@giles17 giles17 added the python Usage: [Issues, PRs], Target: Python label Jul 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes FoundryToolbox.as_skills_provider() in the Python agent-framework-foundry-hosting package so the existing disable_caching flag actually affects toolbox skill discovery, avoiding an extra skill://index.json MCP round-trip on every agent run for hosted agents. It does this by composing caching at the toolbox layer (since SkillsProvider intentionally does not auto-cache caller-supplied SkillsSources) and adds a refresh interval option plus targeted unit tests.

Changes:

  • Compose DeduplicatingSkillsSource(CachingSkillsSource(...)) inside FoundryToolbox.as_skills_provider() when caching is enabled, and stop forwarding disable_caching to SkillsProvider (it’s a no-op for caller-supplied sources).
  • Add cache_refresh_interval: timedelta | None to allow periodic re-discovery when toolbox-attached skills change over process lifetime.
  • Add unit tests validating default caching (read once), disabled caching (read every run), and refresh-interval behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py Implements effective discovery caching by wrapping the toolbox skills source in CachingSkillsSource (plus dedup), adds cache_refresh_interval, and updates the docstring to match real behavior.
python/packages/foundry_hosting/tests/test_toolbox.py Adds tests that stub MCPSkillsSource to count discovery calls and verify cached/uncached/refresh behaviors.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _skills.py10903996%314, 584, 1102, 1117, 1119–1120, 1486–1487, 1729, 1758, 2270, 2459, 2967–2968, 3065, 3073, 3078, 3081, 3086, 3106, 3118, 3123, 3218, 3226, 3231, 3234, 3239, 3259, 3268, 3273, 3539–3540, 4124, 4412–4413, 4440–4441, 4448–4449
packages/foundry_hosting/agent_framework_foundry_hosting
   _toolbox.py680100% 
TOTAL44665521588% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
9067 33 💤 0 ❌ 0 🔥 2m 28s ⏱️

@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

FoundryToolbox.as_skills_provider() now wraps _FoundryToolboxSkillsSource in a default shared CachingSkillsSource, but _ToolboxAuth injects get_request_context().platform_headers() on every outbound MCP request and documents that the forwarded per-request call ID lets the Foundry MCP proxy 'resolve the caller context server-side' (python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py:76-80, :106-110). Core CachingSkillsSource uses one shared bucket unless given an isolation selector (python/packages/core/agent_framework/_skills.py:3775-3782, :3850-3856), so once one hosted request populates the cache, later callers silently skip re-discovery and can see the wrong skill set. The existing provider contract explicitly avoids auto-caching caller-suplied sources for exactly this cross-context leak risk (python/packages/core/agent_framework/_skills.py:2047-2054, python/packages/core/tests/core/test_skills.py:603-6052).


Source: automated DevFlow PR review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 91%

✓ Correctness

The PR correctly fixes the ineffective disable_caching parameter by composing caching wrappers directly in as_skills_provider(). The API usage of CachingSkillsSource and DeduplicatingSkillsSource matches their actual constructors. Removing disable_caching from the SkillsProvider() call is correct since SkillsProvider explicitly does not auto-cache caller-supplied sources. The safety argument (context-independence of _FoundryToolboxSkillsSource) is valid — its get_skills ignores SkillsSourceContext and always returns the same toolbox skills. No correctness issues found.

✓ Security Reliability

This PR correctly fixes the ineffective disable_caching parameter by composing CachingSkillsSource and DeduplicatingSkillsSource directly in as_skills_provider(). The security argument is sound: _FoundryToolboxSkillsSource.get_skills() ignores the SkillsSourceContext (only reads self._toolbox.session), so a shared unkeyed cache cannot leak skills across agents/tenants. The concurrency model in CachingSkillsSource (per-key asyncio locks with double-check) is correct. No injection risks, resource leaks, or unhandled failure modes introduced.

✓ Test Coverage

The PR adds three well-targeted unit tests covering the three key behaviors: default caching (index read once), disabled caching (re-read every call), and cache_refresh_interval threading (zero interval causes re-reads). Tests correctly monkeypatch MCPSkillsSource with a counting stub to verify composition logic without network I/O. The test helper structure is clean and reusable. Coverage is adequate for the scope of the change — the core CachingSkillsSource and DeduplicatingSkillsSource classes have their own independent test suites, so the toolbox tests rightly focus on verifying correct composition/wiring rather than re-testing caching semantics.

✓ Failure Modes

The change correctly composes caching at the call site where it has effect, rather than relying on SkillsProvider which deliberately skips auto-caching for caller-supplied sources. Exception propagation through CachingSkillsSource and DeduplicatingSkillsSource is clean (no swallowed errors). The removed disable_caching argument to SkillsProvider was verified as a no-op for caller-supplied SkillsSource instances. No silent failure modes, lost errors, or operational hazards introduced.

✗ Design Approach

I found one blocking design issue: the new default cache is shared across all callers even though toolbox skill discovery still depends on Foundry’s per-request hosting context, so the first caller’s skill://index.json can be replayed to later callers instead of being re-evaluated for them.

Flagged Issues

  • FoundryToolbox.as_skills_provider() now wraps _FoundryToolboxSkillsSource in a default shared CachingSkillsSource, but _ToolboxAuth injects get_request_context().platform_headers() on every outbound MCP request and documents that the forwarded per-request call ID lets the Foundry MCP proxy 'resolve the caller context server-side' (python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py:76-80, :106-110). Core CachingSkillsSource uses one shared bucket unless given an isolation selector (python/packages/core/agent_framework/_skills.py:3775-3782, :3850-3856), so once one hosted request populates the cache, later callers silently skip re-discovery and can see the wrong skill set. The existing provider contract explicitly avoids auto-caching caller-suplied sources for exactly this cross-context leak risk (python/packages/core/agent_framework/_skills.py:2047-2054, python/packages/core/tests/core/test_skills.py:603-6052).

Automated review by giles17's agents

Emphasize that the toolbox advertises the same skill set to every caller (the
per-request call-id governs execution/authorization, not which skills are
listed) rather than leaning on 'ignores SkillsSourceContext'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84150ec4-6f7c-4ef8-b9fb-12fa11652773
@giles17
giles17 marked this pull request as ready for review July 15, 2026 22:12

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 93%

✓ Correctness

The PR correctly composes caching at the as_skills_provider() level by wrapping the source in DeduplicatingSkillsSource(CachingSkillsSource(...)) when caching is enabled. The constructor signatures of CachingSkillsSource (accepts refresh_interval: timedelta | None) and DeduplicatingSkillsSource (accepts inner_source: SkillsSource) match the PR's usage. Omitting disable_caching from the SkillsProvider call is safe since it defaults to False and is a no-op for caller-supplied sources anyway. No correctness issues found.

✓ Security Reliability

The PR correctly fixes the ineffective disable_caching parameter by composing caching directly in as_skills_provider(). The implementation is sound: CachingSkillsSource properly handles refresh_interval with monotonic timestamps and per-key async locks, DeduplicatingSkillsSource wraps it correctly, and the safety argument for shared caching (context-independence of _FoundryToolboxSkillsSource) is valid. The removal of disable_caching from the SkillsProvider call is correct since it was always a no-op for caller-supplied sources. No security or reliability issues identified.

✓ Test Coverage

The test coverage for this PR is good. Three new tests cover the main behavioral scenarios: default caching (index read once), disabled caching (read every call), and cache_refresh_interval wiring (zero interval forces re-reads). Assertions are meaningful (exact invocation counts), and the monkeypatch approach appropriately isolates the unit under test. The tests verify the wiring of as_skills_provider() without redundantly re-testing CachingSkillsSource internals.

✓ Failure Modes

The PR correctly composes caching at the call site where it can actually take effect, rather than forwarding a no-op parameter to SkillsProvider. The failure-mode surface is minimal: CachingSkillsSource handles refresh_interval=None (never-expire) correctly, the session-None check in _FoundryToolboxSkillsSource is preserved, and the removed disable_caching kwarg to SkillsProvider was always inert for caller-supplied sources. No silent failures, lost errors, or operational failure paths introduced.

✓ Design Approach

The new caching layer makes disable_caching observable, but it also caches MCPSkill objects that are bound to a specific MCP session. Because the MCP tool can reconnect by replacing tool.session, later runs may reuse cached skills that still point at the old closed session, breaking load_skill/read_skill_resource after reconnects.


Automated review by giles17's agents

@giles17
giles17 marked this pull request as draft July 17, 2026 15:16
Cached MCPSkill objects captured the MCP ClientSession at construction, so
after a FoundryToolbox reconnect (which replaces its session) load_skill and
read_skill_resource would fail against the closed session. This regressed once
as_skills_provider() started caching discovery by default.

Add an optional session_provider callable to MCPSkillsSource and MCPSkill
(exactly one of client or session_provider). When supplied, the session is
resolved on every fetch, mirroring how MCPTool resolves self.session live at
call time. _FoundryToolboxSkillsSource now passes a provider that returns the
toolbox's current session, so cached skills always use the live session.

The fixed client= path is unchanged and backward-compatible. Update core tests,
foundry_hosting tests, and core AGENTS.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84150ec4-6f7c-4ef8-b9fb-12fa11652773
@giles17
giles17 marked this pull request as ready for review July 17, 2026 21:16
@giles17 giles17 added the documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs label Jul 17, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 88% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Failure Modes, Design Approach


Automated review by giles17's agents

ty could not call the provider narrowed from \object\ (Top callable). Type the
captured value as Callable[[], object] and drop the redundant callable() assert.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84150ec4-6f7c-4ef8-b9fb-12fa11652773
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: FoundryToolbox.as_skills_provider() disable_caching is a no-op; toolbox skills re-read every run

2 participants